/**
 * Copyright 2010 Tim Azzopardi
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.log4jdbc.sql.resultsetcollector;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.sf.log4jdbc.sql.jdbcapi.ResultSetSpy;

public class DefaultResultSetCollector implements ResultSetCollector {

    private static final String NULL_RESULT_SET_VAL = "[null]";
    private static final String UNREAD = "[unread]";
    private static final String UNREAD_ERROR = "[unread!]";
    private boolean fillInUnreadValues = false;

    public DefaultResultSetCollector(boolean fillInUnreadValues) {
        this.reset();
        this.fillInUnreadValues = fillInUnreadValues;
        this.lastValueReturnedByNext = true;
    }

    /**
     * A {@code boolean} defining whether parameters of this {@code DefaultResultSetColector} 
     * has already been obtained from a {@code ResultSetMetaData}.
     */
    private boolean loaded;
    /**
     * An <code>int</code> representing the number of columns 
     * in the real <code>ResultSet</code> object, obtained by calling 
     * <code>getColumnCount</code> on the related <code>ResultSetMetaData</code> object.
     * This attribute is an <code>Integer</code> rather than an <code>int</code>, 
     * so that we can distinguish the case where the column count has not yet been obtained, 
     * from the case when the column count is 0 (should never happend, but still...)
     */
    private int columnCount;
    /**
     * A <code>Map</code> where the key is an <code>Integer</code> representing 
     * the index of a column, and the corresponding value is a <code>String</code> 
     * being the label of the column. 
     * This <code>Map</code> is populated by calling <code>getColumnLabel</code> 
     * on the <code>ResultSetMetaData</code> object of the real <code>ResultSet</code> object, 
     * for each column (see {@link #columnCount}).
     */
    private Map<Integer, String> columnLabels;
    /**
     * A <code>Map</code> where the key is an <code>Integer</code> representing 
     * the index of a column, and the corresponding value is a <code>String</code> 
     * being the name of the column. 
     * This <code>Map</code> is populated by calling <code>getColumnLabel</code> 
     * on the <code>ResultSetMetaData</code> object of the real <code>ResultSet</code> object, 
     * for each column (see {@link #columnCount}).
     */
    private Map<Integer, String> columnNames;
    /**
     * A <code>boolean</code> that is the last value returned by a call 
     * to <code>next</code> (or <code>first</code>) on the related <code>ResultSet</code>. 
     * This allows to know if this <code>ResultSetCollector</code> must be printed 
     * when <code>close</code> is called on the related <code>ResultSet</code>: 
     * if the previous call to <code>next</code> returned <code>true</code>, 
     * then this <code>ResultSetCollector</code> must be printed 
     * on the call to <code>close</code>; otherwise, it was already printed following 
     * the previous call to <code>next</code>, and should not be printed 
     * on the call to <code>close</code>.
     * <p>
     * This attribute must not be reset after a printing (the other attributes are, through a call 
     * to <code>reset</code>), in case the related <code>ResultSet</code> is reused. 
     * <p>
     * Default value at initialization is <code>true</code> (<code>ResultSetCollector</code> 
     * not printed).
     */
    private boolean lastValueReturnedByNext;
    private List<Object> row;
    private List<List<Object>> rows;
    private Map<String, Integer> colNameToColIndex;
    private int colIndex;
    private static final List<String> GETTERS = Arrays.asList(new String[] { "getString", "getLong", "getInt",
            "getDate", "getTimestamp", "getTime", "getBigDecimal", "getFloat", "getDouble", "getByte", "getShort",
            "getObject", "getBoolean", });

    public List<List<Object>> getRows() {
        return rows;
    }

    public int getColumnCount() {
        return columnCount;
    }

    public void reset() {
        loaded = false;
        rows = null;
        row = null;
        colNameToColIndex = null;
        colIndex = -1;// Useful for wasNull calls
        columnCount = 0;
        columnLabels = new HashMap<Integer, String>();
        columnNames = new HashMap<Integer, String>();
    }

    @Override
    public void loadMetaDataIfNeeded(ResultSet rs) {
        // if data already loaded
        if (this.loaded) {
            return;
        }
        // otherwise, get all data now, so that we don't need
        // to use the ResultSetMetaData later (with some drivers,
        // it cannot be used once the ResultSet has been closed)
        try {
            // if (!rs.isClosed()) {
            this.loadMetaDataIfNeeded(rs.getMetaData());
            // }
        } catch (SQLException e) {
            // as we cannot check that the ResultSet is closed before calling getMetaData
            // in JDBC 3, we are going to assume that if there is an Exception,
            // it is because the ResultSet is closed, and we do nothing.
            // throw new RuntimeException(e);
        }
        this.loaded = true;
    }

    /**
     * Perform the operations requested by {@link #loadMetaDataIfNeeded(ResultSet)}, 
     * using directly the related <code>ResultSetMetaData</code>.
     * @param metaData 	The <code>ResultSetMetaData</code> through which the information 
     * 					needed by {@link #loadMetaDataIfNeeded(ResultSet)} are obtained. 
     */
    private void loadMetaDataIfNeeded(ResultSetMetaData metaData) {
        // if data already loaded
        if (this.loaded) {
            return;
        }
        // otherwise, get all data now, so that we don't need
        // to use the ResultSetMetaData later (with some drivers,
        // it cannot be used once the ResultSet has been closed)
        try {
            if (metaData == null) {
                this.columnCount = 0;
            } else {
                this.columnCount = metaData.getColumnCount();
            }
            this.colNameToColIndex = new HashMap<String, Integer>(this.columnCount);
            for (int column = 1; column <= this.columnCount; column++) {
                String label = metaData.getColumnLabel(column).toLowerCase();
                String name = metaData.getColumnName(column).toLowerCase();
                this.columnLabels.put(column, label);
                this.columnNames.put(column, name);
                colNameToColIndex.put(label, column);
                colNameToColIndex.put(name, column);
            }
            this.loaded = true;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public String getColumnName(int column) {
        return this.columnNames.get(column);
    }

    public String getColumnLabel(int column) {
        return this.columnLabels.get(column);
    }

    /*
     * (non-Javadoc)
     * 
     * @see net.sf.log4jdbc.ResultSetCollector#methodReturned(net.sf.log4jdbc. ResultSetSpy, java.lang.String,
     * java.lang.Object, java.lang.Object, java.lang.Object)
     */
    @Override
    public boolean methodReturned(ResultSetSpy resultSetSpy, String methodCall, Object returnValue,
            Object targetObject, Object... methodParams) {

        if (methodCall.startsWith("get") && methodParams != null && methodParams.length == 1) {

            String methodName = methodCall.substring(0, methodCall.indexOf('('));
            if (GETTERS.contains(methodName) && getColumnCount() != 0) {
                setColIndexFromGetXXXMethodParams(methodParams);
                makeRowIfNeeded();
                row.set(colIndex - 1, returnValue);
            }
        }
        if (methodCall.equals("wasNull()") && getColumnCount() != 0) {
            if (Boolean.TRUE.equals(returnValue)) {
                //row.set(colIndex - 1, NULL_RESULT_SET_VAL);
            }
        }
        if ("next()".equals(methodCall) || "first()".equals(methodCall)) {
            this.lastValueReturnedByNext = (Boolean) returnValue;
        }
        if ("next()".equals(methodCall) || "first()".equals(methodCall) || "close()".equals(methodCall)) {
            loadMetaDataIfNeeded(resultSetSpy.getRealResultSet());
            boolean isEndOfResultSet =
            // "close" triggers a printing only if the previous call to next
            // did not return false (end of result set already reached)
                    ("close()".equals(methodCall) && this.lastValueReturnedByNext != false)
                            || Boolean.FALSE.equals(returnValue);
            if (row != null) {
                if (rows == null)
                    rows = new ArrayList<List<Object>>();
                rows.add(row);
                row = null;
            }
            if (isEndOfResultSet) {
                return true;
            }
        }
        // TODO: Tim: if prev() called, warn about no support for reverse cursors

        if ("getMetaData()".equals(methodCall)) {
            // If the client code calls getMetaData then we don't have to
            // here we assume that the real ResultSet is not yet closed.
            this.loadMetaDataIfNeeded((ResultSetMetaData) returnValue);
        }
        return false;
    }

    private void makeRowIfNeeded() {
        if (row == null) {
            row = new ArrayList<Object>(getColumnCount());
            for (int i = 0; i < getColumnCount(); ++i) {
                row.add(UNREAD);
            }
        }
    }

    private void setColIndexFromGetXXXMethodParams(Object... methodParams) {
        Object param1 = methodParams[0];
        if (param1 == null) {
            throw new RuntimeException("ResultSet.getXXX() first param null? ");
        }
        if (param1 instanceof Integer) {
            colIndex = (Integer) param1;
        } else if (param1 instanceof String) {
            if (colNameToColIndex == null) {
                throw new RuntimeException("ResultSet.getXXX(colName): colNameToColIndex null");
            }
            Integer idx = colNameToColIndex.get(((String) param1).toLowerCase());

            if (idx == null) {
                throw new RuntimeException("ResultSet.getXXX(colName): could not look up name");
            }
            colIndex = idx;
        } else {
            throw new RuntimeException("ResultSet.getXXX called with: " + param1.getClass().getName());
        }
    }

    @Override
    public void preMethod(ResultSetSpy resultSetSpy, String methodCall, Object... methodParams) {
        if ((methodCall.equals("next()") || methodCall.equals("close()")) && fillInUnreadValues) {
            if (row != null) {
                int colIndex = 0;
                for (Object v : row) {
                    if (v != null && v.toString().equals(UNREAD)) {
                        Object resultSetValue = null;
                        try {
                            // Fill in any unread data
                            resultSetValue = JdbcUtils.getResultSetValue(resultSetSpy.getRealResultSet(), colIndex + 1);
                        } catch (SQLException e) {
                            resultSetValue = UNREAD_ERROR;
                        }
                        if (resultSetValue != null) {
                            row.set(colIndex, resultSetValue);
                        } else {
                            row.set(colIndex, NULL_RESULT_SET_VAL);
                        }
                    }
                    colIndex++;
                }
            }
        }
    }

}
